package com.dysoft.flesh.gl;

import com.dysoft.bones.Application;
import com.dysoft.bones.DataManager;
import com.dysoft.flesh.RenderContext;
import com.dysoft.flesh.Renderer;
import com.dysoft.flesh.Shader;

import javax.media.opengl.GL;
import javax.media.opengl.GLContext;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Sean Micklethwaite
 *         Aug 14, 2010 8:56:11 PM
 */
public class GLShader extends GLObject implements Shader {
	final Collection<AbstractUniform<?>> uniforms = new ArrayList<AbstractUniform<?>>();
	final Map<Type, Component> shaders = new HashMap<Type, Component>();
	boolean isCompiled = false;

	public GLShader() {
		super(GLContext.getCurrent().getGL().glCreateProgram());
	}

	public GLShader(String source) throws IOException {
		this();
		attachShader(fromFile(source + ".frag"));
		attachShader(fromFile(source + ".vert"));
		compile();
	}

	public GLShader attachShader(Component shader) {
		GL gl = GLContext.getCurrent().getGL();

		if(isCompiled && shaders.containsKey(shader.getType())) {
			gl.glDetachShader(handle, shaders.get(shader.getType()).getHandle());
		}
		shaders.put(shader.getType(), shader);
		return this;
	}

	public GLShader compile() {
		GL gl = GLContext.getCurrent().getGL();

		for(Component shader : shaders.values()) {
			gl.glAttachShader(handle, shader.getHandle());
		}
		gl.glLinkProgram(handle);
		gl.glValidateProgram(handle);
		if(isCompiled) {
			for(AbstractUniform<?> u : uniforms) {
				u.refresh();
			}
		}
		isCompiled = true;
		return this;
	}

	public void activate(RenderContext ctx) throws DataManager.DataObject.NotPresentException {
		((GLRenderer.GLRenderContext)ctx).getGL().glUseProgram(handle);
		for(AbstractUniform u : uniforms) u.update();
	}

	public void deactivate(RenderContext ctx) throws DataManager.DataObject.NotPresentException {
		((GLRenderer.GLRenderContext)ctx).getGL().glUseProgram(0);
	}

	public Shader.Component getComponent(Type type) {
		return shaders.get(type);
	}

	public Collection<? extends Uniform<?>> getUniforms() {
		return uniforms;
	}

	public boolean isCompiled() {
		return isCompiled;
	}

	@Override
	protected void finalize() throws Throwable {
		dtor(new Renderer.Operation() {
			public boolean call(RenderContext ctx) throws Exception {
				((GLRenderer.GLRenderContext)ctx).getGL().glDeleteProgram(handle);
				return false;
			}
		});
		super.finalize();
	}

	public Uniform<Integer> uniform(String name, int val) {
		return uniform(new AbstractUniform<Integer>(name, val) {
			@Override
			void update() {
				GLContext.getCurrent().getGL().glUniform1i(handle, val);
			}
		});
	}

	public Uniform<Float> uniform(String name, float val) {
		return uniform(new AbstractUniform<Float>(name, val) {
			@Override
			void update() {
				GLContext.getCurrent().getGL().glUniform1f(handle, val);
			}
		});
	}

	public Uniform<Float []> uniform(String name, Float [] val) {
		switch(val.length) {
			case 2:
				return uniform(new AbstractUniform<Float []>(name, val) {
					@Override
					void update() {
						GLContext.getCurrent().getGL().glUniform2f(handle, val[0], val[1]);
					}
				});
			case 3:
				return uniform(new AbstractUniform<Float []>(name, val) {
					@Override
					void update() {
						GLContext.getCurrent().getGL().glUniform3f(handle, val[0], val[1], val[2]);
					}
				});
			case 4:
				return uniform(new AbstractUniform<Float []>(name, val) {
					@Override
					void update() {
						GLContext.getCurrent().getGL().glUniform4f(handle, val[0], val[1], val[2], val[3]);
					}
				});
			default:
				throw new RuntimeException("Unsupported array size: " + val.length);
		}
	}

	protected <T> AbstractUniform<T> uniform(AbstractUniform<T> u) {
		uniforms.add(u);
		return u;
	}

	protected abstract class AbstractUniform<T> implements Uniform<T> {
		protected T val;
		protected final String name;
		protected int handle;

		protected AbstractUniform(String name, T val) {
			this.val = val;
			this.name = name;
			refresh();
		}

		public T getVal() {
			return val;
		}

		public void setVal(T val) {
			this.val = val;
		}

		public String getName() {
			return name;
		}

		abstract void update();

		public void refresh() {
			handle = GLContext.getCurrent().getGL().glGetUniformLocation(GLShader.this.handle, name);
		}

		@Override
		public String toString() {
			return name;
		}
	}

	public static class Component extends GLObject implements Shader.Component {
		final String source;
		final Type type;

		public Component(String source, Type type) {
			super(GLContext.getCurrent().getGL().glCreateShader(
				type.equals(Type.VERTEX_SHADER) ? GL.GL_VERTEX_SHADER
				: type.equals(Type.FRAGMENT_SHADER) ? GL.GL_FRAGMENT_SHADER
				: GL.GL_GEOMETRY_SHADER_EXT
			));
			this.source = source;
			this.type = type;

			GL gl = GLContext.getCurrent().getGL();

			gl.glShaderSource(handle, 1, new String [] {source}, null);
			gl.glCompileShader(handle);

			int [] iv = new int [1];
			gl.glGetShaderiv(handle, GL.GL_COMPILE_STATUS, iv, 0);

			if(iv[0] != GL.GL_TRUE) {
				gl.glGetShaderiv(handle, GL.GL_INFO_LOG_LENGTH, iv, 0);
				ByteBuffer infoLog = ByteBuffer.allocate(iv[0]);
				gl.glGetShaderInfoLog(handle, iv[0], IntBuffer.wrap(iv), infoLog);
				StringBuilder sb = new StringBuilder();
				sb.append("Compilation failed ").append(iv[0]).append('\n');
				while(infoLog.hasRemaining()) {
					sb.append((char)infoLog.get());
				}
				sb.append("Source listing:\n").append(source);
				System.out.println(sb);
			}
		}

		public String getSource() {
			return source;
		}

		public Type getType() {
			return type;
		}

		@Override
		protected void finalize() throws Throwable {
			dtor(new Renderer.Operation() {
				public boolean call(RenderContext ctx) throws Exception {
					((GLRenderer.GLRenderContext)ctx).getGL().glDeleteShader(handle);
					return false;
				}
			});
			super.finalize();
		}
	}

	public static Component fromFile(InputStream file, Type type) throws IOException {
		return new Component(Application.readEntireFile(file), type);
	}

	public static Component fromFile(String file, Type type) throws IOException {
		return fromFile(Application.readFile(file), type);
	}

	public static Component fromFile(String file) throws IOException {
		Type type = file.endsWith(".vert") ? Type.VERTEX_SHADER
				: file.endsWith(".geom") ? Type.GEOMETRY_SHADER
				: Type.FRAGMENT_SHADER;
		return fromFile(file, type);
	}
}
